import * as pbkiller from "../pbkiller/src/pbkiller"
import { CWebSocket } from "./CWebSocket"
import { JKManager } from "../JKManager"

let ReconectWaitingTime = 10,
    MaxReconnectTimes = 3, // 重连次数
    RequestTimeout = 10, // 超时时间，秒
    HeartFrequence = 5, // 心跳间隔时间，秒
    SocketState = {
        Closed: 0,
        Connecting: 1,
        Connected: 2,
    }

type GetData = {
    type: `GET`,
    param?: {
        [key: string]: unknown
    },
}
type PostData = {
    type: `POST`,
    param?: unknown,
}

export type HttpResponse = {
    code: number,
    data?: unknown,
    msg?: string,
}

/**
 * 网络管理器单例
 */
export class NetManager {
    readonly HttpError = {
        Error: 1000,
        TimeOut: 1001,
    }

    private _reconnectTimes: number
    private _requestingSet: Set<unknown>
    private _socketState: number
    private _protoBuf: any
    private _socket: CWebSocket
    private _frequence: number
    private _heartStartTime: number
    private _isReLogin: boolean
    private _heartTimeoutId: number
    private connectOption: string | { protocol: string, ip: string, port: string }
    private _requestingSetHttp: { [key: string]: Function[] }

    init(call: () => void = null): void {
        cc.log(`${cc.js.getClassName(this)} init`)

        this._reconnectTimes = MaxReconnectTimes
        this._requestingSet = new Set()
        this._requestingSetHttp = cc.js.createMap()

        this._socketState = SocketState.Closed

        this._loadProtoBuf(call)
    }

    // ====================================================================================
    // ===================================== 内部接口 ======================================
    // ====================================================================================

    private _loadProtoBuf(call: () => void): void {
        cc.log(`${cc.js.getClassName(this)} _loadProtoBuf`)

        pbkiller.preload(() => {
            this._protoBuf = pbkiller.loadAll()
            call && call()
        })
    }

    // ==================================== WebSocket =====================================

    /** 
     * 监听Socket事件 
     */
    private _addEventToSocket(): void {
        cc.log(`${cc.js.getClassName(this)} _addEventToSocket`)

        // 连接成功
        this._socket.onConnect = (event: Event) => {
            cc.log(`${cc.js.getClassName(this)} socket onConnect`)

            this._socketState = SocketState.Connected
            this._requestingSet = new Set()
            JKManager.GEventManager.emit(`ConnectSocket`, {})
            this._reconnectTimes = MaxReconnectTimes

            // 心跳
            clearTimeout(this._heartTimeoutId)
            this._heartTimeoutId = null
            this._frequence = HeartFrequence * 1000
            this._heartStartTime = Date.now()
            let delayTime = 0,
                heartFunc = (noTimeout: boolean = false) => {
                    let time = Date.now(),
                        offsetTime = time - this._heartStartTime
                    cc.log(`${cc.js.getClassName(this)} this._heartStartTime = ${this._heartStartTime} time = ${time} offsetTime = ${offsetTime}`)
                    if (!noTimeout) {
                        delayTime = offsetTime - this._frequence
                    }
                    cc.log(`${cc.js.getClassName(this)} delayTime1 = ${delayTime}`)
                    delayTime = delayTime >= 0 ? delayTime : 0
                    let timeout = this._frequence - delayTime
                    this._heartStartTime = Date.now()
                    this._socketState === SocketState.Connected && this.sendSocketMsg(`GateOfHeartConnectedRequest`, {}, RequestTimeout, false)
                    clearTimeout(this._heartTimeoutId)
                    this._heartTimeoutId = null
                    this._heartTimeoutId = setTimeout(() => {
                        heartFunc()
                    }, timeout)
                    cc.log(`${cc.js.getClassName(this)} delayTime2 = ${delayTime}`)
                }
            if (this._heartTimeoutId === null) {
                heartFunc(true)
            }
            cc.game.targetOff(this)
            cc.game.on(cc.game.EVENT_HIDE, () => {
                cc.log(`${cc.js.getClassName(this)} EVENT_HIDE`)

                clearTimeout(this._heartTimeoutId)
                this._heartTimeoutId = null
            }, this)
            cc.game.on(cc.game.EVENT_SHOW, () => {
                cc.log(`${cc.js.getClassName(this)} EVENT_SHOW`)

                heartFunc(true)
            }, this)
        }

        // 收到消息
        this._socket.onMessage = (buffer) => {
            let msg = this._protoBuf.HongXiuMessageReply.decode(buffer)
            if (msg && msg.reply) {
                let key = msg.reply,
                    msgId = key.substring(0, 1).toUpperCase() + key.substring(1, key.length),
                    deleteKey = msgId.slice(0, msgId.length - 5)
                if (this._requestingSet.has(deleteKey)) {
                    this._requestingSet.delete(deleteKey)
                }
                let info = msg[key]
                cc.log(`${cc.js.getClassName(this)} socket onMessage msgId:${msgId} resultCode:${msg.resultCode} info:`, info)

                if (
                    msg.resultCode !== 12 &&
                    msg.resultCode !== 200 &&
                    msg.resultCode !== 201 &&
                    msg.resultCode !== JKManager.ProtoEnum.ResultCode.SUCCESS
                ) {
                    JKManager.UiManager.showPopupView(`Prefab/Core/CommonDialogView/CommonDialogView`, {
                        msg: `resultCode${msg.resultCode}: ${JKManager.Language.getText(`errMsg${msg.resultCode}`)}`,
                        callConfirm: () => {
                            cc.log(`${cc.js.getClassName(this)} socket callConfirm`)
                        },
                    })
                }

                JKManager.GEventManager.emit(msgId, {
                    resultCode: msg.resultCode,
                    data: info || {},
                })
            } else {
                if (msg.resultCode !== 1) {
                    let key = msg.reply,
                        msgId = key.substring(0, 1).toUpperCase() + key.substring(1, key.length)
                    msgId !== `GateOfHeartConnectedRequest` && cc.log(`${cc.js.getClassName(this)} socket onMessage buffer:`, msg)
                } else {
                    // let accountInfo = JKManager.AccountManager.getAccountInfo()
                    // JKManager.NetManager.sendSocketMsg(`AccountOfLoginRequest`, {
                    //     accountId: accountInfo.account,
                    //     password: accountInfo.password,
                    // })
                }
            }
        }

        // 连接被关闭
        this._socket.onClose = (event: Event) => {
            cc.log(`${cc.js.getClassName(this)} socket onClose`)

            this._socketState = SocketState.Closed
            clearInterval(this._heartTimeoutId)
            this._heartTimeoutId = undefined
            this._tryReconectSocket()

            return
        }

        // 网络错误
        this._socket.onError = (event: Event) => {
            cc.log(`${cc.js.getClassName(this)} socket onError`)
        }
    }

    /**
     * 尝试重连
     */
    private _tryReconectSocket(): void {
        cc.log(`${cc.js.getClassName(this)} socket _tryReconectSocket`)

        if (this._reconnectTimes > 0) {
            this._reconnectTimes--
            setTimeout(() => {
                this.connectSocket(this.connectOption)
            }, ReconectWaitingTime * 1000)
        } else {
            JKManager.UiManager.showPopupView(`Prefab/Core/CommonDialogView/CommonDialogView`, {
                msg: `${JKManager.Language.getText(`msgReconect`)}`,
                callConfirm: () => {
                    cc.log(`${cc.js.getClassName(this)} socket callConfirm`)

                    this._reconnectTimes = MaxReconnectTimes
                    this.connectSocket(this.connectOption)
                },
            })
        }
    }

    // ======================================= Http ========================================

    /**
     * 发送http请求
     * @param url 地址
     * @param data 请求类型
     * @param callback 请求成功或失败回调
     * @param timeout 超时时间，秒，有默认值
     * @param isJoinRequestList 是否加入请求队列，默认是，队列中已有的请求在得到回复之前，后续相同的请求会被忽略
     */
    private _httpSend(url: string, data: GetData | PostData, callback: (code: number, response: HttpResponse) => void = null, timeout: number = RequestTimeout, isJoinRequestList: boolean = true): void {
        cc.log(`${cc.js.getClassName(this)} _httpSend url = ${url} data = `, data, ` timeout = ${timeout} isJoinRequestList = ${isJoinRequestList}`)

        if (url.indexOf(`http`) === 0) {
            this._requestingSetHttp[url] = this._requestingSetHttp[url] || []
            if (isJoinRequestList) {
                if (this._requestingSetHttp[url] && this._requestingSetHttp[url].length) {
                    cc.warn(`${cc.js.getClassName(this)} _httpSend The http request url:${url} can not be record again!`)

                    this._requestingSetHttp[url].push(callback)
                } else {
                    this._requestingSetHttp[url] = [callback]
                }
            }
            let type = data.type,
                param = data.param,
                xhr = new XMLHttpRequest(),
                json: string = null
            xhr.onreadystatechange = () => {
                if (xhr.readyState == 4 && (xhr.status >= 200 && xhr.status < 400)) {
                    let response = xhr.responseText,
                        canParse = true,
                        rsp: HttpResponse,
                        code: number,
                        data: unknown,
                        msg: string
                    try {
                        rsp = JKManager.Utils.parseJSON(response) as HttpResponse
                        code = rsp.code
                        data = rsp.data
                        msg = rsp.msg
                    } catch (err) {
                        console.error(`${cc.js.getClassName(this)} _httpSend onreadystatechange ${err}`)
                        canParse = false
                    }
                    if (canParse && code === 200) {
                        cc.log(`${cc.js.getClassName(this)} _httpSend onreadystatechange `, rsp)

                        this._requestingSetHttp[url].forEach(callback => {
                            callback(code, response && JKManager.Utils.copyObj(rsp))
                        })
                        !isJoinRequestList && callback(code, response && JKManager.Utils.copyObj(rsp))
                        this._requestingSetHttp[url] = []
                    } else {
                        cc.error(`${cc.js.getClassName(this)} _httpSend onreadystatechange `, rsp)

                        this._requestingSetHttp[url].forEach(callback => {
                            callback(this.HttpError.Error, null)
                        })
                        !isJoinRequestList && callback(this.HttpError.Error, null)
                        this._requestingSetHttp[url] = []
                    }
                } else {
                    // cc.error(`${cc.js.getClassName(this)} _httpSend onreadystatechange xhr.readyState = ${xhr.readyState} xhr.status = ${xhr.status}`)
                }
            }
            xhr.onerror = () => {
                console.error(`${cc.js.getClassName(this)} _httpSend onerror xhr.readyState = ${xhr.readyState} status = ${xhr.status}`)

                this._requestingSetHttp[url].forEach(callback => {
                    callback(xhr.status, null)
                })
                !isJoinRequestList && callback(xhr.status, null)
                this._requestingSetHttp[url] = []
            }
            xhr.timeout = timeout * 1000
            xhr.ontimeout = () => {
                console.error(`${cc.js.getClassName(this)} _httpSend ontimeout`)

                this._requestingSetHttp[url].forEach(callback => {
                    callback(this.HttpError.TimeOut, null)
                })
                !isJoinRequestList && callback(this.HttpError.TimeOut, null)
                this._requestingSetHttp[url] = []
            }
            if (type === `GET`) {
                url += `?`
                Object.keys(param).forEach(key => {
                    url += `${key}=${param[key]}&`
                })
            } else {
                json = JSON.stringify(param || {})
            }
            xhr.open(type, url, true)
            xhr.setRequestHeader(`Access-Control-Allow-Origin`, `*`)
            xhr.setRequestHeader(`Content-Type`, `application/json`)
            if (cc.sys.isNative) {
                xhr.setRequestHeader(`Accept-Encoding`, `gzip,deflate`)
            }
            xhr.send(json)
        } else {
            cc.log(`${cc.js.getClassName(this)} _httpSend url is unkown!`)
        }
    }

    // ====================================================================================
    // ===================================== 外部接口 ======================================
    // ====================================================================================

    isPbLoaded(): boolean {
        return !!this._protoBuf
    }

    // ==================================== WebSocket =====================================

    isConnectingSocket(): boolean {
        return this._socketState == SocketState.Connecting
    }

    isConnectedSocket(): boolean {
        return this._socketState == SocketState.Connected
    }

    connectSocket(connectOption: string | { protocol: string, ip: string, port: string }): void {
        cc.log(`${cc.js.getClassName(this)} connectSocket`)

        if (!this._isReLogin) {
            if (!this._socket) {
                this._socket = new CWebSocket()
                this._addEventToSocket()
            }
            this.connectOption = connectOption
            if (this._socket && this._socketState === SocketState.Closed) {
                this._socket.connect(this.connectOption)
            }
            this._socketState = SocketState.Connecting // 连接中
        }
    }

    closeSocket(): void {
        cc.log(`${cc.js.getClassName(this)} closeSocket`)

        this._socket.close()
        delete this._socket
    }

    /**
     * 发送Socket消息
     * @param msgId proto定义的Message名称,不做包名解析
     * @param data 数据
     * @param timeout 超时时间，秒，有默认值
     * @param isJoinRequestList 是否加入请求队列，默认是，队列中已有的请求在得到回复之前，后续相同的请求会被忽略
     */
    sendSocketMsg(msgId: string, data: object = null, timeout: number = RequestTimeout, isJoinRequestList: boolean = true): boolean {
        msgId !== `GateOfHeartConnectedRequest` && cc.log(`${cc.js.getClassName(this)} sendSocketMsg msgId = ${msgId} timeout = ${timeout} isJoinRequestList = ${isJoinRequestList}`)

        let subRequest = new this._protoBuf[msgId](data)
        if (subRequest) {
            let subMsgId: string
            for (const key in data) {
                if (data.hasOwnProperty(key)) {
                    if (key.indexOf(`Request`) >= 0) {
                        subMsgId = key

                        break
                    }
                }
            }
            let key = msgId.slice(0, msgId.length - 7)
            if (this._requestingSet.has(key)) {
                cc.warn(`${cc.js.getClassName(this)} sendSocketMsg The message msgId:${msgId} can not be record again!`)
            } else {
                msgId !== `GateOfHeartConnectedRequest` && cc.log(`sendSocketMsg msgId:${msgId} data:`, data)

                let request = new this._protoBuf.HongXiuMessageRequest()
                let str1 = msgId.substring(0, 1).toLowerCase()
                let str2 = msgId.substring(1, msgId.length)
                key = str1 + str2
                request[key] = subRequest
                // request.platform = JKManager.AppConfig.Platform
                let buffer = request.toBuffer()
                // msgId !== `GateOfHeartConnectedRequest` && cc.log(buffer)
                let msgIdKey = (subMsgId && subMsgId.slice(0, msgId.length - 7)) || msgId.slice(0, msgId.length - 7)
                if (isJoinRequestList) {
                    this._requestingSet.add(msgIdKey)
                    setTimeout(() => {
                        this._requestingSet && this._requestingSet.delete(msgIdKey)
                    }, timeout * 1000)
                }

                return this._socket.send(buffer)
            }
        } else {
            cc.error(`${cc.js.getClassName(this)} sendSocketMsg There is no msgId:${msgId} in protoBuf!`)
        }

        return false
    }

    /**
     * 监听消息
     * @param msgId 消息
     * @param callback 回调
     * @param target 回调上下文
     * @param once 是否一次性监听，默认否
    */
    onSocketMsg(msgId: string, callback: (...data: unknown[]) => void, target: unknown, once: boolean = false): void {
        JKManager.GEventManager.on(msgId, callback, target, once)
    }

    /**
     * 注销监听消息
     * @param msgId 消息
     * @param callback 回调
     * @param target 回调上下文
     */
    offSocketMsg(msgId: string, callback: (...data: unknown[]) => void, target: unknown): void {
        JKManager.GEventManager.off(msgId, callback, target)
    }

    /**
     * 设置是否被别处登录
     * @param bool 是否被别处登录
     */
    setReLogin(bool: boolean): void {
        cc.log(`${cc.js.getClassName(this)} setReLogin`)

        this._isReLogin = bool
    }

    // ======================================= Http ========================================

    /**
     * 发起Http GET请求，一般的资源请求，请使用cc.as
     * @param url 地址
     * @param callback 请求成功或失败回调
     * @param data 参数对象
     * @param timeout 超时时间，秒，有默认值
     * @param isJoinRequestList 是否加入请求队列，默认是，队列中已有的请求在得到回复之前，后续相同的请求会被忽略
     */
    httpGet(url: string | URL, callback: (code: number, response: HttpResponse) => void, data: { [key: string]: unknown } = null, timeout: number = RequestTimeout, isJoinRequestList: boolean = true): void {
        timeout = timeout || RequestTimeout
        cc.log(`${cc.js.getClassName(this)} httpGet url = ${url} data = `, data, ` timeout = ${timeout} isJoinRequestList = ${isJoinRequestList}`)

        if (url) {
            let oUrl: string
            if (url instanceof URL) {
                oUrl = url.href
            } else {
                oUrl = url
            }
            this._httpSend(oUrl, {
                type: `GET`,
                param: data,
            }, callback, timeout, isJoinRequestList)
        }
    }

    /**
     * 发起Http POST请求
     * @param url 地址
     * @param callback 请求成功或失败回调
     * @param data 参数对象
     * @param timeout 超时时间，秒，有默认值
     * @param isJoinRequestList 是否加入请求队列，默认是，队列中已有的请求在得到回复之前，后续相同的请求会被忽略
     */
    httpPost(url: string | URL, callback: (code: number, response: HttpResponse) => void, data: unknown = null, timeout: number = RequestTimeout, isJoinRequestList: boolean = true): void {
        timeout = timeout || RequestTimeout
        cc.log(`${cc.js.getClassName(this)} httpPost url = ${url} data = `, data, ` timeout = ${timeout} isJoinRequestList = ${isJoinRequestList}`)

        if (url) {
            let oUrl: string
            if (url instanceof URL) {
                oUrl = url.href
            } else {
                oUrl = url
            }
            this._httpSend(oUrl, {
                type: `POST`,
                param: data,
            }, callback, timeout, isJoinRequestList)
        }
    }

    /**
     * 加载网络图片
     * @param url 图片地址，如果是相对地址，则会拼接系统配置的资源服地址
     * @param handleTarget 操作对象，精灵组件或回调，如果不传则为变相的预下载
     * @param useCache 是否使用缓存，默认是
     * @param ext 拓展名 png jpg
     */
    loadNetImg(url: string, handleTarget: cc.Sprite | ((spf: cc.SpriteFrame) => void) = null, useCache = true, ext: `png` | `jpg` = `png`) {
        if (handleTarget instanceof cc.Sprite) {
            // cc.log(`${cc.js.getClassName(this)} loadNetImg url = ${url} handleTarget.node.name = ${handleTarget.node.name}`)
        } else {
            // cc.log(`${cc.js.getClassName(this)} loadNetImg url = ${url} handleTarget = Function`)
        }

        if (url) {
            if (url[url.length - 1] === `/`) {
                // cc.log(`loadNetImg invalid url"${url}" end with"/"`)

                return
            }
            url = `${url}${useCache ? `` : `?time=${Date.now()}`}`
            // cc.log(`${cc.js.getClassName(this)} loadNetImg url = ${url}`)
            cc.assetManager && cc.assetManager.loadRemote(url, { ext: `.${ext}` }, (err, tex: cc.Texture2D) => {
                if (err) {
                    cc.error(`${cc.js.getClassName(this)} loadNetImg`, err)
                    if (handleTarget instanceof Function) {
                        handleTarget(null)
                    }
                } else if (handleTarget) {
                    if (handleTarget instanceof cc.Sprite) {
                        if (handleTarget.node && cc.isValid(handleTarget.node, true)) {
                            handleTarget.spriteFrame = new cc.SpriteFrame(tex)
                        }
                    } else {
                        handleTarget(new cc.SpriteFrame(tex))
                    }
                }
            })
        }
    }

    /**
     * 加载网络JSON文件
     * @param url 文件地址，如果是相对地址，则会拼接系统配置的资源服地址
     * @param call 回调，如果不传则为变相的预下载
     * @param useCache 是否使用缓存，默认是
     */
    loadNetJSON(url: string, call: ((json: object) => void), useCache = true) {
        cc.log(`${cc.js.getClassName(this)} loadNetJSON url = ${url} useCache = ${useCache}`)

        if (url) {
            if (url[url.length - 1] === `/`) {
                cc.log(`loadNetJSON invalid url"${url}" end with"/"`)
            } else {
                url = `${url}${useCache ? `` : `?time=${Date.now()}`}`
                cc.log(`${cc.js.getClassName(this)} loadNetJSON url = ${url}`)
                cc.assetManager && cc.assetManager.loadRemote(url, { ext: `.json` }, (err, file: cc.JsonAsset) => {
                    if (err) {
                        cc.error(`${cc.js.getClassName(this)} loadNetJSON`, err)
                        call(null)
                    } else if (call) {
                        call(JSON.parse(file.json))
                    }
                })
            }
        }
    }
}